package trip

import (
	"context"
	rentalpb "coolcar/v1/rental/api/gen/v1"
	"coolcar/v1/rental/trip/dao"
	"coolcar/v1/shared/auth"
	"coolcar/v1/shared/ids"
	"coolcar/v1/shared/mgutils/objid"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"math/rand"
	"time"
)

// Service defines a trip service.
type Service struct {
	Sugar *zap.SugaredLogger
	Mongo *dao.Mongo
	ProfileManager
	CarManager
	POIManager
}

// ProfileManager defines the ACL (Anti Corruption Layer)
type ProfileManager interface {
	Verify(ctx context.Context, id ids.AccountID) (ids.IdentityID, error)
}

// CarManager defines the ACL.
type CarManager interface {
	Verify(ctx context.Context, cid ids.CarID, location *rentalpb.Location) error
	Unlock(ctx context.Context, cid ids.CarID, aid ids.AccountID, tid ids.TripID, avatarURL string) error
	Lock(ctx context.Context, cid ids.CarID) error
}

// POIManager resolves POI(Point Of Interest).
type POIManager interface {
	Resolve(ctx context.Context, location *rentalpb.Location) (string, error)
}

func (s *Service) CreateTrip(ctx context.Context, request *rentalpb.CreateTripRequest) (*rentalpb.TripEntity, error) {
	aid, err := auth.AccountIDFromContext(ctx)
	if err != nil {
		return nil, err
	}
	// 验证驾驶者身份
	iiD, err := s.ProfileManager.Verify(ctx, aid)
	if err != nil {
		return nil, status.Error(codes.FailedPrecondition, err.Error())
	}
	if request.CarId == "" || request.Start == nil {
		return nil, status.Error(codes.InvalidArgument, "")
	}
	// 检查车辆状态
	cid := ids.CarID(request.CarId)
	err = s.CarManager.Verify(ctx, cid, request.Start)
	if err != nil {
		return nil, status.Error(codes.FailedPrecondition, err.Error())
	}
	// 创建行程: 写入数据库,开始计费
	poi, err := s.POIManager.Resolve(ctx, request.Start)
	if err != nil {
		s.Sugar.Info("failed Resolve poi", zap.Stringer("location", request.Start))
	}

	ls := s.calcCurrentStatus(ctx, &rentalpb.LocationStatus{
		Location:     request.Start,
		PoiName:      poi,
		TimestampSec: nowFunc(),
	}, request.Start)

	tr, err := s.Mongo.CreateTrip(ctx, &rentalpb.Trip{
		AccountId:  aid.String(),
		CarId:      cid.String(),
		Start:      ls,
		Current:    ls,
		Status:     rentalpb.TripStatus_IN_PROGRESS,
		IdentityId: iiD.String(),
	})
	if err != nil {
		s.Sugar.Warn("failed to create Trip", zap.Error(err))
		return nil, status.Error(codes.AlreadyExists, "")
	}
	// 车辆开锁
	go func() {
		err := s.CarManager.Unlock(context.Background(), cid, aid, objid.ToTripID(tr.ID), request.AvatarUrl)
		if err != nil {
			s.Sugar.Error("failed to unlock car", zap.Error(err))
		}
	}()

	return &rentalpb.TripEntity{
		Id:   tr.ID.Hex(),
		Trip: tr.Trip,
	}, nil
}

func (s *Service) GetTrip(ctx context.Context, request *rentalpb.GetTripRequest) (*rentalpb.Trip, error) {
	aid, err := auth.AccountIDFromContext(ctx)
	if err != nil {
		return nil, err
	}
	tid := ids.TripID(request.Id)
	tr, err := s.Mongo.GetTrip(ctx, aid, tid)
	if err != nil {
		return nil, status.Error(codes.NotFound, "")
	}
	return tr.Trip, nil
}

func (s *Service) GetTrips(ctx context.Context, request *rentalpb.GetTripsRequest) (*rentalpb.GetTripsResponse, error) {
	aid, err := auth.AccountIDFromContext(ctx)
	if err != nil {
		return nil, err
	}
	trs, err := s.Mongo.GetTrips(ctx, aid, request.Status)
	if err != nil {
		s.Sugar.Error("failed to get_trips", zap.Error(err))
		return nil, status.Error(codes.Internal, "")
	}
	var res *rentalpb.GetTripsResponse
	for _, tr := range trs {
		res.Trips = append(res.Trips, &rentalpb.TripEntity{
			Id:   tr.ID.Hex(),
			Trip: tr.Trip,
		})
	}

	return res, nil
}

func (s *Service) UpdateTrip(ctx context.Context, request *rentalpb.UpdateTripRequest) (*rentalpb.Trip, error) {
	aid, err := auth.AccountIDFromContext(ctx)
	if err != nil {
		return nil, err
	}
	tid := ids.TripID(request.Id)
	tr, err := s.Mongo.GetTrip(ctx, aid, tid)
	if err != nil {
		s.Sugar.Errorf("failed to get trip: %v", err)
		return nil, status.Error(codes.NotFound, "")
	}
	if tr.Trip.Current == nil {
		s.Sugar.Error("trip without current trip", zap.String("id", tid.String()))
		return nil, status.Error(codes.Internal, "")
	}
	cur := tr.Trip.Current.Location
	if request.Current != nil {
		cur = request.Current
	}
	tr.Trip.Current = s.calcCurrentStatus(ctx, tr.Trip.Current, cur)
	if request.EndTrip {
		tr.Trip.End = tr.Trip.Current
		tr.Trip.Status = rentalpb.TripStatus_FINISHED
		err := s.CarManager.Lock(ctx, ids.CarID(tr.Trip.CarId))
		if err != nil {
			return nil, status.Errorf(codes.FailedPrecondition, "failed to lock car: %v", err)
		}
	}
	err = s.Mongo.UpdateTrip(ctx, tid, aid, tr.UpdatedAt, tr.Trip)
	if err != nil {
		s.Sugar.Errorf("failed to update trip: %v", err)
		return nil, status.Error(codes.Internal, "")
	}
	return tr.Trip, nil
}

var nowFunc = func() int64 {
	return time.Now().Unix()
}

const (
	centsPerSec = 0.8
	kmPerSec    = 0.02
)

func (s *Service) calcCurrentStatus(ctx context.Context, last *rentalpb.LocationStatus, cur *rentalpb.Location) *rentalpb.LocationStatus {
	now := nowFunc()
	elapsedSec := float64(now - last.TimestampSec)
	poi, err := s.POIManager.Resolve(ctx, cur)
	if err != nil {
		s.Sugar.Info("failed to resolve poi", zap.Stringer("location", cur), err.Error())
	}
	return &rentalpb.LocationStatus{
		Location:     cur,
		FeeCent:      last.FeeCent + int32(centsPerSec*elapsedSec*2*rand.Float64()),
		KmDriven:     last.KmDriven + (kmPerSec * elapsedSec * 2 * rand.Float64()),
		PoiName:      poi,
		TimestampSec: now,
	}
}
